:art: Support mini pay relative

FFIB 5 years ago
parent
commit
ad5c8304d1
11 changed files with 295 additions and 0 deletions
  1. 5 0
      api/urls.py
  2. 1 0
      kodosale/settings.py
  3. 0 0
      pay/__init__.py
  4. 10 0
      pay/admin.py
  5. 50 0
      pay/migrations/0001_initial.py
  6. 0 0
      pay/migrations/__init__.py
  7. 74 0
      pay/models.py
  8. 4 0
      pay/tests.py
  9. 138 0
      pay/views.py
  10. 2 0
      requirements_pywe.txt
  11. 11 0
      utils/error/errno_utils.py

+ 5 - 0
api/urls.py

@@ -3,6 +3,7 @@
3 3
 from django.conf.urls import url
4 4
 
5 5
 from api import mini_views, oauth_views, pack_views
6
+from pay import views as pay_views
6 7
 
7 8
 
8 9
 urlpatterns = [
@@ -15,6 +16,10 @@ urlpatterns += [
15 16
 ]
16 17
 
17 18
 urlpatterns += [
19
+    url(r'^pay/wx/order_create$', pay_views.wx_order_create_api, name='wx_order_create_api'),   # 订单创建
20
+]
21
+
22
+urlpatterns += [
18 23
     url(r'^3rd/or$', oauth_views.oauth_redirect, name='3rd_or'),
19 24
     url(r'^3rd/oauth_redirect$', oauth_views.oauth_redirect, name='3rd_oauth_redirect'),
20 25
 ]

+ 1 - 0
kodosale/settings.py

@@ -58,6 +58,7 @@ INSTALLED_APPS = [
58 58
     'account',
59 59
     'goods',
60 60
     'kol',
61
+    'pay',
61 62
 ]
62 63
 
63 64
 MIDDLEWARE = [

+ 0 - 0
pay/__init__.py


+ 10 - 0
pay/admin.py

@@ -0,0 +1,10 @@
1
+from django.contrib import admin
2
+
3
+
4
+# Register your models here.
5
+from pay.models import OrderInfo
6
+
7
+class OrderInfoAdmin(admin.ModelAdmin):
8
+    list_display = ('kol_id', 'pack_id', 'goods_info', 'user_id', 'total_fee', 'name', 'phone', 'address', 'tracking_number', 'pay_status')
9
+
10
+admin.site.register(OrderInfo, OrderInfoAdmin)

+ 50 - 0
pay/migrations/0001_initial.py

@@ -0,0 +1,50 @@
1
+# Generated by Django 2.2.12 on 2020-04-21 15:47
2
+
3
+from django.db import migrations, models
4
+import jsonfield.fields
5
+import shortuuidfield.fields
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+    ]
14
+
15
+    operations = [
16
+        migrations.CreateModel(
17
+            name='OrderInfo',
18
+            fields=[
19
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
+                ('status', models.BooleanField(db_index=True, default=True, help_text='Status', verbose_name='status')),
21
+                ('created_at', models.DateTimeField(auto_now_add=True, help_text='Create Time', verbose_name='created_at')),
22
+                ('updated_at', models.DateTimeField(auto_now=True, help_text='Update Time', verbose_name='updated_at')),
23
+                ('order_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='订单唯一标识', max_length=22)),
24
+                ('prepay_id', models.CharField(blank=True, help_text='预支付交易会话标识', max_length=64, null=True, verbose_name='prepay_id')),
25
+                ('transaction_id', models.CharField(blank=True, help_text='交易单号', max_length=32, null=True, verbose_name='transaction_id')),
26
+                ('pack_id', models.CharField(blank=True, db_index=True, help_text='包唯一标识', max_length=32, null=True, verbose_name='pack_id')),
27
+                ('goods_info', jsonfield.fields.JSONField(blank=True, default=[], help_text='商品信息', null=True, verbose_name='goods_info')),
28
+                ('user_id', models.CharField(blank=True, db_index=True, help_text='用户唯一标识', max_length=32, null=True, verbose_name='kol_id')),
29
+                ('kol_id', models.CharField(blank=True, db_index=True, help_text='kol_id唯一标识', max_length=32, null=True, verbose_name='kol_id')),
30
+                ('body', models.CharField(blank=True, help_text='商品描述', max_length=255, null=True, verbose_name='body')),
31
+                ('total_fee', models.IntegerField(default=0, help_text='总金额', verbose_name='total_fee')),
32
+                ('name', models.CharField(blank=True, help_text='姓名', max_length=255, null=True, verbose_name='name')),
33
+                ('phone', models.CharField(blank=True, help_text='电话', max_length=255, null=True, verbose_name='phone')),
34
+                ('address', models.CharField(blank=True, help_text='地址', max_length=255, null=True, verbose_name='address')),
35
+                ('tracking_number', models.CharField(blank=True, help_text='快递单号', max_length=255, null=True, verbose_name='tracking_number')),
36
+                ('has_send_template_message', models.BooleanField(db_index=True, default=True, help_text='是否已发送模版消息', verbose_name='has_send_template_message')),
37
+                ('trade_type', models.CharField(blank=True, help_text='支付方式', max_length=255, null=True, verbose_name='trade_type')),
38
+                ('pay_status', models.IntegerField(choices=[(0, '待支付'), (1, '已支付'), (2, '已失败')], db_index=True, default=0, help_text='支付状态', verbose_name='pay_status')),
39
+                ('paid_at', models.DateTimeField(blank=True, help_text='支付时间', null=True, verbose_name='paid_at')),
40
+                ('reback_status', models.BooleanField(db_index=True, default=False, help_text='退款状态', verbose_name='reback_status')),
41
+                ('reback_at', models.DateTimeField(blank=True, help_text='退款时间', null=True, verbose_name='reback_at')),
42
+                ('unifiedorder_result', models.TextField(blank=True, help_text='统一下单结果', null=True, verbose_name='unifiedorder_result')),
43
+                ('notify_msg', models.TextField(blank=True, help_text='回调信息', null=True, verbose_name='notify_msg')),
44
+            ],
45
+            options={
46
+                'verbose_name': '订单信息',
47
+                'verbose_name_plural': '订单信息',
48
+            },
49
+        ),
50
+    ]

+ 0 - 0
pay/migrations/__init__.py


+ 74 - 0
pay/models.py

@@ -0,0 +1,74 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.db import models
4
+from django.utils.translation import ugettext_lazy as _
5
+from django_models_ext import BaseModelMixin
6
+from shortuuidfield import ShortUUIDField
7
+from jsonfield import JSONField
8
+from TimeConvert import TimeConvert as tc
9
+
10
+class OrderInfo(BaseModelMixin):
11
+
12
+    """
13
+    # Trade State of Wechat Query
14
+    SUCCESS ——— 支付成功
15
+    REFUND ——— 转入退款
16
+    NOTPAY ——— 未支付
17
+    CLOSED ——— 已关闭
18
+    REVOKED ——— 已撤销(刷卡支付)
19
+    USERPAYING ——— 用户支付中
20
+    PAYERROR ——— 支付失败(其他原因,如银行返回失败)
21
+    """
22
+
23
+    WAITING_PAY = 0
24
+    PAID = 1
25
+    FAIL = 2
26
+    # DELETED = 9
27
+
28
+    PAY_STATUS = (
29
+        (WAITING_PAY, u'待支付'),
30
+        (PAID, u'已支付'),
31
+        (FAIL, u'已失败'),
32
+        # (DELETED, u'已删除'),
33
+    )
34
+
35
+    order_id = ShortUUIDField(_(u'order_id'), max_length=32, help_text=u'订单唯一标识', db_index=True)
36
+
37
+    prepay_id = models.CharField(_(u'prepay_id'), max_length=64, blank=True, null=True, help_text=u'预支付交易会话标识')
38
+    transaction_id = models.CharField(_(u'transaction_id'), max_length=32, blank=True, null=True, help_text=u'交易单号')
39
+
40
+    pack_id = models.CharField(_(u'pack_id'), max_length=32, blank=True, null=True, help_text=u'包唯一标识', db_index=True)
41
+    goods_info = JSONField(_('goods_info'), default=[], blank=True, null=True, help_text='商品信息')
42
+
43
+    user_id = models.CharField(_(u'kol_id'), max_length=32, blank=True, null=True, help_text=u'用户唯一标识', db_index=True)
44
+    kol_id = models.CharField(_(u'kol_id'), max_length=32, blank=True, null=True, help_text=u'kol_id唯一标识', db_index=True)
45
+
46
+    body = models.CharField(_(u'body'), max_length=255, blank=True, null=True, help_text=u'商品描述')
47
+    total_fee = models.IntegerField(_(u'total_fee'), default=0, help_text=u'总金额')
48
+
49
+    name = models.CharField(_(u'name'), max_length=255, blank=True, null=True, help_text=u'姓名')
50
+    phone = models.CharField(_(u'phone'), max_length=255, blank=True, null=True, help_text=u'电话')
51
+    address = models.CharField(_(u'address'), max_length=255, blank=True, null=True, help_text=u'地址')
52
+
53
+    tracking_number = models.CharField(_(u'tracking_number'), max_length=255, blank=True, null=True, help_text=u'快递单号')
54
+    has_send_template_message = models.BooleanField(_(u'has_send_template_message'), default=True, help_text=u'是否已发送模版消息', db_index=True)
55
+
56
+    trade_type = models.CharField(_(u'trade_type'), max_length=255, blank=True, null=True, help_text=u'支付方式')
57
+
58
+    pay_status = models.IntegerField(_(u'pay_status'), choices=PAY_STATUS, default=WAITING_PAY, help_text=u'支付状态', db_index=True)
59
+    paid_at = models.DateTimeField(_(u'paid_at'), blank=True, null=True, help_text=_(u'支付时间'))
60
+
61
+    reback_status = models.BooleanField(_(u'reback_status'), default=False, help_text=u'退款状态', db_index=True)
62
+    reback_at = models.DateTimeField(_(u'reback_at'), blank=True, null=True, help_text=_(u'退款时间'))
63
+
64
+    # 微信统一下单
65
+    unifiedorder_result = models.TextField(_(u'unifiedorder_result'), blank=True, null=True, help_text=_(u'统一下单结果'))
66
+    # 微信支付回调
67
+    notify_msg = models.TextField(_(u'notify_msg'), blank=True, null=True, help_text=u'回调信息')
68
+
69
+    class Meta:
70
+        verbose_name = _(u'订单信息')
71
+        verbose_name_plural = _(u'订单信息')
72
+
73
+    def __unicode__(self):
74
+        return u'{0.pk}'.format(self)

+ 4 - 0
pay/tests.py

@@ -0,0 +1,4 @@
1
+from django.test import TestCase
2
+
3
+
4
+# Create your tests here.

+ 138 - 0
pay/views.py

@@ -0,0 +1,138 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf import settings
4
+from django.db import transaction
5
+from django.shortcuts import HttpResponse
6
+from django_logit import logit
7
+from django_response import response
8
+from pywe_exception import WeChatPayException
9
+from pywe_pay import WeChatPay
10
+from pywe_pay_notify import check_pay_notify
11
+from pywe_response import WXPAY_NOTIFY_FAIL, WXPAY_NOTIFY_SUCCESS
12
+from pywe_sign import check_signature
13
+from TimeConvert import TimeConvert as tc
14
+import json
15
+from functools import reduce
16
+
17
+from account.models import UserInfo
18
+from goods.models import PackInfo, PackGoodsInfo, GoodsInfo
19
+from kol.models import KOLInfo
20
+from pay.models import OrderInfo
21
+from utils.error.errno_utils import (PackStatusCode, KOLStatusCode, PackGoodsStatusCode, OrderStatusCode, UserStatusCode, 
22
+                                     WithdrawStatusCode)
23
+
24
+
25
+WECHAT = settings.WECHAT
26
+
27
+
28
+@logit
29
+@transaction.atomic
30
+def wx_order_create_api(request):
31
+    """ 订单创建 """
32
+    kol_id = request.POST.get('kol_id', '')
33
+    user_id = request.POST.get('user_id', '')
34
+    pack_id = request.POST.get('pack_id', '')
35
+    goods_info = json.loads(request.POST.get('goods_info', '[]'))
36
+
37
+    name = request.POST.get('name', '')
38
+    phone = request.POST.get('phone', '')
39
+    address = request.POST.get('address', '')
40
+
41
+    # 用户校验
42
+    try:
43
+        user = UserInfo.objects.get(user_id=user_id, status=True)
44
+    except UserInfo.DoesNotExist:
45
+        return response(UserStatusCode.USER_NOT_FOUND)
46
+
47
+    # kol校验
48
+    try:
49
+        kol = KOLInfo.objects.get(kol_id=kol_id, status=True)
50
+    except KOLInfo.DoesNotExist:
51
+        return response(KOLStatusCode.KOL_NOT_FOUND)
52
+    
53
+    # 包校验
54
+    try:
55
+        pack = PackInfo.objects.get(kol_id=kol_id, pack_id=pack_id, status=True)
56
+    except PackInfo.DoesNotExist:
57
+        return response(PackStatusCode.PACK_NOT_FOUND)
58
+
59
+    body = request.POST.get('body', '')  # 商品描述
60
+    total_fee = int(request.POST.get('total_fee', 0))  # 总金额,单位分
61
+
62
+    #包-商品校验
63
+    for g in goods_info:
64
+        try:
65
+            PackGoodsInfo.objects.get(pack_id=pack_id, good_id=g.get('good_id', ''))
66
+        except PackGoodsInfo.DoesNotExist:
67
+            return response(PackGoodsStatusCode.PACK_GOODS_NOT_FOUND)
68
+
69
+    # 金额校验
70
+    if reduce(lambda g1, g2: g1.price * g1.num + g2.price * g2.num, goods_info) != total_fee:
71
+        return response(OrderStatusCode.FEE_CHECK_FAIL)
72
+
73
+    # JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付,统一下单接口trade_type的传参可参考这里
74
+    trade_type = 'JSAPI'
75
+
76
+    # 根据 trade_type 获取 wechat 配置
77
+    wxcfg = WECHAT.get(trade_type, {})
78
+    # WeChatPay 初始化
79
+    wxpay = WeChatPay(wxcfg.get('appID'), wxcfg.get('apiKey'), wxcfg.get('mchID'))
80
+
81
+    # 生成订单
82
+    order = OrderInfo.objects.create(
83
+        user_id=user_id,
84
+        pack_id=pack_id,
85
+        kol_id=kol_id,
86
+        goods_info=goods_info,
87
+        total_fee=total_fee,
88
+        trade_type=trade_type,
89
+        name=name,
90
+        phone=phone,
91
+        address=address,
92
+    )
93
+
94
+    try:
95
+        prepay_data = wxpay.order.create(
96
+            body=body,
97
+            notify_url=settings.API_DOMAIN + '/wx/notify_url',
98
+            out_trade_no=order.order_id,
99
+            total_fee=total_fee,
100
+            trade_type=trade_type,
101
+            openid=user.openid,  # 可选,用户在商户appid下的唯一标识。trade_type=JSAPI,此参数必传
102
+        )
103
+    except WeChatPayException as e:
104
+        order.unifiedorder_result = e.message
105
+        order.save()
106
+        return response(OrderStatusCode.WX_UNIFIED_ORDER_FAIL)
107
+
108
+    prepay_id = prepay_data.get('prepay_id', '')
109
+    order.prepay_id = prepay_id
110
+    order.save()
111
+
112
+    if trade_type == 'JSAPI' or trade_type == 'MINIAPP':
113
+        wxpay_params = wxpay.jsapi.get_jsapi_params(prepay_id)
114
+    elif trade_type == 'APP':
115
+        wxpay_params = wxpay.order.get_appapi_params(prepay_id)
116
+
117
+    return response(200, 'Order Create Success', u'订单创建成功', {
118
+        'order_id': order.order_id,
119
+        'prepay_id': prepay_id,
120
+        'wxpay_params': wxpay_params,
121
+    })
122
+
123
+
124
+def order_paid_success(order):
125
+    if order.pay_status == OrderInfo.PAID:
126
+        return
127
+    
128
+    order.pay_status = OrderInfo.PAID
129
+    order.paid_at = tc.utc_datetime()
130
+    order.save()
131
+
132
+
133
+def order_paid_fail(order):
134
+    if order.pay_status == OrderInfo.FAIL:
135
+        return
136
+
137
+    order.pay_status = OrderInfo.FAIL
138
+    order.save()

+ 2 - 0
requirements_pywe.txt

@@ -1,3 +1,5 @@
1 1
 pywe_miniapp==1.1.5
2 2
 pywe-oauth==1.1.1
3 3
 pywe-pay==1.0.13
4
+pywe-pay-notify==1.0.4
5
+pywe-response==1.0.1

+ 11 - 0
utils/error/errno_utils.py

@@ -2,6 +2,17 @@
2 2
 
3 3
 from StatusCode import BaseStatusCode, StatusCodeField
4 4
 
5
+class UserStatusCode(BaseStatusCode):
6
+    USER_NOT_FOUND = StatusCodeField(400001, 'User Not Found', description=u'用户不存在')
7
+
8
+class KOLStatusCode(BaseStatusCode):
9
+    KOL_NOT_FOUND = StatusCodeField(400001, 'KOL Not Found', description=u'KOL不存在')
10
+
11
+class PackStatusCode(BaseStatusCode):
12
+    PACK_NOT_FOUND = StatusCodeField(100001, 'Pack Not Found', description=u'包不存在')
13
+
14
+class PackGoodsStatusCode(BaseStatusCode):
15
+    PACK_GOODS_NOT_FOUND = StatusCodeField(100001, 'Pack Goods Not Found', description=u'包商品不存在')
5 16
 
6 17
 class ParamStatusCode(BaseStatusCode):
7 18
     """ 4000xx 参数相关错误码 """